﻿using Foundation.Core;
using Foundation.EntityFrameworkCore;
using Foundation.Uow;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using SaveChangesFailedEventArgs = Foundation.EntityFrameworkCore.SaveChangesFailedEventArgs;

namespace Dyao.Book.Repository.Shared
{
    public abstract class BizDbContext<TDbContext> : FoundationDbContext<TDbContext>, IFoundationEfCoreDbContext, ITransientDependency
         where TDbContext : DbContext
    {
        private EntityPropertySetter _entityPropertySetter;

        protected EntityPropertySetter EntityPropertySetter => ServiceProvider.GetRequiredServiceReference(ref _entityPropertySetter);

        protected IServiceProvider ServiceProvider { get; private set; }

        public event EventHandler<SaveChangesFailedEventArgs> SaveChangesFailed;

        protected BizDbContext(DbContextOptions<TDbContext> options, IServiceProvider serviceProvider)
           : base(options)
        {
            ServiceProvider = serviceProvider;
        }

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            var entries = this.ChangeTracker.Entries().ToList();

            ApplyCompleProperties(entries);

            try
            {
                return base.SaveChanges(acceptAllChangesOnSuccess);
            }
            catch (Exception ex)
            {
                SetFlagIfConcurrency(ex);

                if (SaveChangesFailed != null)
                {
                    SaveChangesFailed.Invoke(this, new SaveChangesFailedEventArgs(this, entries, ex));
                }

                throw;
            }
        }

        public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
        {
            var entries = this.ChangeTracker.Entries().ToList();
            ApplyCompleProperties(entries);

            try
            {
                var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);

                return result;
            }
            catch (Exception ex)
            {
                SetFlagIfConcurrency(ex);

                if (SaveChangesFailed != null)
                {
                    SaveChangesFailed.Invoke(this, new SaveChangesFailedEventArgs(this, entries, ex));
                }

                throw;
            }
        }

        /// <summary>
        /// 设置创建、更新的用户及时间等信息
        /// </summary>
        protected virtual void ApplyCompleProperties(IList<EntityEntry> entries)
        {
            foreach (var entityEntry in entries)
            {
                if (entityEntry.State == EntityState.Detached || entityEntry.State == EntityState.Unchanged)
                {
                    continue;
                }

                EntityPropertySetter.SetProperties(entityEntry);
            }
        }

        /// <summary>
        /// 如果 <paramref name="exception"/> 为 <see cref="DbUpdateConcurrencyException"/> 异常，设置同步异常标记
        /// </summary>
        /// <param name="exception"></param>
        protected void SetFlagIfConcurrency(Exception exception)
        {
            if (exception is DbConcurrencyException || exception is DbUpdateConcurrencyException)
            {
                exception.Data[Dyao.Book.Core.ExceptionExtensions.DbConcurrencyExceptionKey] = true;
            }
        }

        public override void Initialize(FoundationEfCoreDbContextInitializationContext initializationContext)
        {
            base.Initialize(initializationContext);

            var unitOfWork = initializationContext.UnitOfWork;

            if (!unitOfWork.Options.IsReadDb)
            {
                var container = unitOfWork.GetOrAddItem(EntityTrackingConstants.EntityTrackingContainerKey, (key) =>
                {
                    return ServiceProvider.GetService(typeof(IEntityTrackingDbContextContainer)) as IEntityTrackingDbContextContainer;
                });

                SaveChangesFailed += container.OnSaveChangesFailed;
                container.AddDbContext(this);
            }
        }

    }
}
